<!-- 分片上传 -->
<template>
  <div class="dialog-box">
    <el-dialog
      v-model="visible"
      title="上传概况"
      width="700"
      align-center
      @closed="handleCancel"
    >
      <div class="overview">
        <span :style="`color: ${uploadStatusColor.get(1)}`"
          >上传进度：{{
            Math.floor((successNumber / totalNumber) * 1000) / 10
          }}%</span
        >
        <span :style="`color: ${uploadStatusColor.get(3)}`"
          >已上传：{{ successNumber }}</span
        >
        <span :style="`color: ${uploadStatusColor.get(0)}`"
          >待上传：{{ awaitNumber }}</span
        >
        <span :style="`color: ${uploadStatusColor.get(2)}`"
          >上传失败：{{ errorNumber }}</span
        >
      </div>
      <div class="progress-list">
        <div v-for="(item, i) in chunkList" :key="item.index" class="item">
          <span class="name">分片{{ item.index }}：</span>
          <el-progress
            class="progress"
            :percentage="item.progress"
            :color="uploadProgress.get(item.status)"
          />
          <span
            class="status"
            :style="`color: ${uploadStatusColor.get(item.status)}`"
            >{{ uploadStatus.get(item.status) }}</span
          >
        </div>
      </div>
      <div slot="footer" class="dialog-footer">
        <el-button @click="handleCancel">返回</el-button>
        <el-button type="primary" @click="handleOk">确定</el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script setup lang="ts" name="sliceUpload">
import { ref } from "vue";
import * as api from "@/api/index";
import { postAction } from "@/api/request";
import { getFileHash } from "@/uilts/uilts-core";
import { ElMessage as message } from "element-plus";
// 分片数据类型
interface chunkType {
  fileName: string;
  index: number | string;
  key?: string;
  chunk: Blob;
  progress: number;
  status: number;
}
let emit = defineEmits(["handleProgress"]);
const uploadStatus = new Map([
  [0, "等待上传"],
  [1, "上传中"],
  [2, "上传失败"],
  [3, "上传成功"],
]);
const uploadStatusColor = new Map([
  [0, "#409EFF"],
  [1, "#E6A23C"],
  [2, "#F56C6C"],
  [3, "#529b2e"],
]);
const uploadProgress = new Map([
  [0, "warning"],
  [1, "format"],
  [2, "exception"],
  [3, "#b3e19d"],
]);
// 弹窗控制器
let visible = ref(false);

// 已上传数量
let successNumber = ref(0);

// 上传失败数量
let errorNumber = ref(0);

// 等待上传数量
let awaitNumber = ref(0);

// 总上传数量
let totalNumber = ref(1);

// 切片大小 bit
let silceSize = 1024 * 100 * 2;

// 源文件
let file: any = ref(null);

// 切片数据
let chunkList = ref<chunkType[]>([]);

// 返回给父组件的数据 如果还没上传完成 返回upload状态的文件对象 上传完成返回done转台的文件对象
let uploadFile = ref<UploadFile>({
  uid: "",
  name: "",
  status: "uploading",
  url: "",
});

// 源文件的key 当拿到key时 请求是否有该文件上传任务 有则获取进度 没有则切片上传
let fileKey: any = ref("");

// 校验后的上传状态 0 -> 未上传 1 -> 上传中 2 -> 已上传
let vaildStatus = ref(0);

// 父级调用 打开弹窗
const handleShow = async (params: any) => {
  visible.value = params.visible;
  file.value = params.file;
  if (file.value) {
    await fileSlice();
    beforeVaild();
  }
};

/**
 * 获取到文件key(hash)后调用 查询是否存在该文件上传任务
 * 1. 不存在 -> 执行切片上传
 * 2. 存在但未完成 -> 获取上传进度 继续上传(断点续传)
 * 3. 存在且已完成 -> 直接完成(秒传)
 */
const beforeVaild = () => {
  api.upload
    .blogUploadVaild({ key: fileKey.value })
    .then((res: BaseResponse<any>) => {
      if (res.header.code === 999) {
        if (res.body.length === 0) {
          // 开始切片上传
          vaildStatus.value = 0;
          // 触发上传操作
          chunkUpload();
        } else if (res.body[0].src) {
          // 秒传
          vaildStatus.value = 2;
          // 分片待上传设置为0
          awaitNumber.value = 0;
          // 全部分片上传成功
          successNumber.value = chunkList.value.length;
          chunkList.value.forEach((item: chunkType) => {
            item.status = 3;
            item.progress = 100;
          });
          let data = res.body[0];
          // 设置返回数据
          uploadFile.value = {
            uid: data.id,
            name: data.name,
            url: data.src,
            status: "success",
            response: data,
            percentage: 100,
            size: file.value.size,
            raw: file.value,
          };
          emit("handleProgress", uploadFile.value);
        } else {
          // 续传
          vaildStatus.value = 1;
          // 过滤出未上传的分片
          let list = chunkFilter(res.body.map((item: any) => item.index));
          if (list.length > 0) {
            // 将还未上传的分片重新触发上传
            chunkUpload(list);
          }
        }
      }
    });
};

// 分片上传 list -> 需要上传的分片
const chunkUpload = (list?: chunkType[]) => {
  let requeryList: any = [];
  let chunkLists = list || chunkList.value;
  chunkLists.forEach((item: chunkType) => {
    let data = new FormData();
    data.append("chunk", item.chunk);
    data.append("tag", fileKey.value);
    data.append("key", item.index as string);
    postAction("/note/big-file-slice-upload", data, {
      neverCancel: true,
      Headers: {
        "Content-Type": "multipart/form-data",
      },
      onUploadProgress: (e: any) => {
        console.log(e, "ee");
        item.status = 1;
        item.progress = Math.ceil((e.loaded / e.total) * 10000) / 100;
        if (item.progress >= 100) {
          // 状态改为上传成功
          item.status = 3;
        }
      },
    }).then((res: BaseResponse<any>) => {
      if (res.header.code === 999) {
        // 待上传分片减一
        awaitNumber.value -= 1;
        // 上传成功分片加一
        successNumber.value += 1;
        // 每个分片上传完成 更新返回上传进度
        uploadFile.value = {
          uid: 1,
          name: file.value.name,
          url: undefined,
          status: "uploading",
          percentage: (successNumber.value / chunkList.value.length) * 100,
          size: file.value.size,
          raw: file.value,
        };
        emit("handleProgress", uploadFile.value);
      } else {
        // 待上传分片减一
        awaitNumber.value -= 1;
        // 上传失败分片加一
        errorNumber.value += 1;
        // 分片状态改为上传失败
        item.status = 2;
      }

      console.log("上传完成");
    });
  });
  Promise.all(requeryList).then((list) => {
    // 当所有分片上传完成触发合并操作
    postAction("/note/big-file-merge", {
      filename: file.value.name,
      chunkLen: chunkList.value.length,
      tag: fileKey.value,
      fileSize: file.value.size,
    }).then((res) => {
      if (res.header.code === 999) {
        message.success("文件上传成功");
      }
    });
  });
};

// 筛选待上传的分片
const chunkFilter = (chunkIndex: string[]) => {
  let list = chunkList.value.filter((item: chunkType) => {
    if (chunkIndex.includes(item.index + "")) {
      // 状态改为上传成功
      item.status = 3;
      // 待上传分片减一
      awaitNumber.value -= 1;
      // 上传成功分片加一
      successNumber.value += 1;
      // 上传进度设为100%
      item.progress = 100;
    }
    return !chunkIndex.includes(item.index + "");
  });
  return list;
};

// 文件切片 and 获取文件 hash
const fileSlice = async () => {
  // 计算切片数量
  totalNumber.value = Math.ceil(file.value.size / silceSize);
  // 切片方法
  let fileSlice = File.prototype.slice;
  // 切片起点
  let startSlice = 0;
  // 切片终点
  let endSlice = silceSize;
  for (let i = 0; i < totalNumber.value; i++) {
    // 每次循环重新计算切片终点
    endSlice =
      (i + 1) * silceSize > file.value.size
        ? file.value.size
        : (i + 1) * silceSize;
    let chunk = {
      fileName: file.value.name,
      index: i + 1,
      chunk: fileSlice.call(file.value, startSlice, endSlice),
      progress: 0,
      status: 0, // 0->等待上传 1->上传中 2->上传失败 3->上传完成
    };
    // 切片完更新起点
    startSlice = endSlice;
    // 将切片信息保存
    chunkList.value.push(chunk);
  }
  // 设置待上传数量
  awaitNumber.value = chunkList.value.length;
  // 获取文件hash
  fileKey.value = await getFileHash(chunkList.value, 2);
  return Promise.resolve(fileKey.value);
};

// 关闭弹窗
const handleCancel = () => {
  // 关闭弹窗
  visible.value = false;
  // 初始化分片数组
  chunkList.value = [];
  // 初始化文件hash
  fileKey.value = "";
  // 初始化文件
  file.value = null;
  // 初始化上传数量
  successNumber.value = 0;
  errorNumber.value = 0;
  awaitNumber.value = 0;
  totalNumber.value = 1;
};

// 确认
const handleOk = () => {
  handleCancel();
};

// 对外暴露的属性or方法 否则父级无法调用
defineExpose({
  handleShow,
});
</script>

<style lang="less" scoped>
:deep(.el-dialog__body) {
  padding: 0px 24px 24px 24px;
}
.dialog-box {
  .overview {
    display: flex;
    height: 40px;
    justify-content: space-around;
    align-items: center;
    box-shadow: 0px 5px 5px rgba(134, 134, 133, 0.1);
  }
  .progress-list {
    height: 460px;
    overflow-y: auto;
    .item {
      display: flex;
      height: 40px;
      justify-content: space-around;
      align-items: center;
      .name {
        display: inline-block;
        width: 80px;
        text-align: right;
        padding-right: 12px;
        box-sizing: border-box;
      }
      .progress {
        flex: 1;
      }
      .status {
        width: 80px;
      }
    }
  }
  .dialog-footer {
    margin-top: 16px;
    display: flex;
    height: 40px;
    justify-content: flex-end;
    align-items: center;
  }
}
</style>
